﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections;
using System.Drawing.Design;
using AjaxSnippets.Design;
using System.ComponentModel.Design;

namespace AjaxSnippets
{
    /// <summary>
    /// Describes a server callback that is registered with the client-side component SnippetManager,
    /// once registered controls in the snippet such as buttons can be used to trigger it
    /// </summary>
    [ParseChildren(true)]
    [ToolboxData("<{0}:SnippetCallback runat=server></{0}:SnippetCallback>")]
    [Designer(typeof(SnippetCallbackDesigner))]
    [PersistChildren(false)]
    [NonVisualControl]
    public class SnippetCallback : Control
    {
        /// <summary>
        /// The virtual path to the the webservice asmx that contains the web service
        /// method to call
        /// </summary>
        [Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
        [Bindable(true)]
        [DefaultValue("")]
        [UrlProperty]
        [Browsable(true)]
        public string WebServiceVirtualPath { get; set; }

        /// <summary>
        /// The webservice method to call
        /// </summary>
        [Browsable(true)]
        public string WebServiceMethodName { get; set; }

        /// <summary>
        /// Optional, if this is specified then the callback methods will
        /// be executed on a client component rather than the client window object
        /// </summary>
        [Browsable(true)]
        public string CallbackComponentId { get; set; }

        /// <summary>
        /// A client function that will be invoked before the callback.
        /// If CallbackComponentId is provided then the function will
        /// be invoked on the given component
        /// </summary>
        [Browsable(true)]
        public string BeforeCallbackMethodName { get; set; }


        /// <summary>
        /// A client function that will be invoked on callback success.
        /// If CallbackComponentId is provided then the function will
        /// be invoked on the given component
        /// </summary>
        [Browsable(true)]
        public string CallbackSuccessMethodName { get; set; }

        /// <summary>
        /// The client function to invoke on callback failure.
        /// If CallbackComponentId is provided then the function will
        /// be invoked on the given component
        /// </summary>
        [Browsable(true)]
        public string CallbackFailureMethodName { get; set; }

        private CallbackParameterMappingCollection _ParameterMapping = new CallbackParameterMappingCollection();

        /// <summary>
        /// A collection of callback parameter mappings that define how a dom elements value will be
        /// extracted and which webservice method argument its mapped to
        /// </summary>
        [MergableProperty(false)]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Editor(typeof(CollectionEditor), typeof(UITypeEditor))]
        public CallbackParameterMappingCollection ParameterMapping
        {
            get { return _ParameterMapping; }
        }

        private CallbackTriggerCollection _Triggers = new CallbackTriggerCollection();

        /// <summary>
        /// A collection of triggers that define which web controls in the snippet can trigger
        /// a callback
        /// </summary>
        [MergableProperty(false)]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Editor(typeof(CollectionEditor), typeof(UITypeEditor))]
        public CallbackTriggerCollection Triggers
        {
            get { return _Triggers; }
        }

        protected override void OnPreRender(EventArgs e)
        {
            EnsureRequiredProperties();

            AttachCallbackTriggerClientEvents();

            base.OnPreRender(e);
        }
        protected override void Render(HtmlTextWriter writer)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
            writer.AddAttribute("language", "javascript");
            writer.RenderBeginTag(HtmlTextWriterTag.Script);

            if (!IsOnSnippetPage) writer.Write("Sys.Application.add_load(function() {\n");

            writer.Write("AjaxSnippets.SnippetManager.registerCallback(");
            writer.Write("'" + ID + "'");
            writer.Write(",'" + ResolveUrl(WebServiceVirtualPath) + "'");
            writer.Write(",'" + WebServiceMethodName + "'");

            if (String.IsNullOrEmpty(CallbackComponentId))
                writer.Write(",null");
            else
                writer.Write(",'" + CallbackComponentId + "'");

            if (String.IsNullOrEmpty(BeforeCallbackMethodName))
                writer.Write(",null");
            else
                writer.Write(",'" + BeforeCallbackMethodName + "'");

            writer.Write(",'" + CallbackSuccessMethodName + "'");
            if (String.IsNullOrEmpty(CallbackFailureMethodName))
                writer.Write(",null");
            else
                writer.Write(",'" + CallbackFailureMethodName + "'");

            writer.Write(",[");

            for (int i = 0; i < ParameterMapping.Count; i++)
            {
                ResolveParameterMappingProperties(ParameterMapping[i]);

                writer.Write(ParameterMapping[i].ToJson());

                if (i < ParameterMapping.Count - 1)
                {
                    writer.Write(",");

                }
            }
            writer.Write("]);\n");

            if (!IsOnSnippetPage) writer.Write("});\n");

            writer.RenderEndTag();
        }

        /// <summary>
        /// Resolves ClientID and ClientValueExtractorFunction from the ControlID if possible
        /// </summary>
        /// <param name="mapping"></param>
        private void ResolveParameterMappingProperties(CallbackParameterMapping mapping)
        {
            if (!String.IsNullOrEmpty(mapping.ControlID))
            {
                Control control = FindControl(mapping.ControlID);
                if (control == null)
                    throw new InvalidProgramException("Error in ParameterMapping, a control with the id of " + mapping.ControlID + " could not be found");

                if (Config.Extractors.ContainsKey(control.GetType().FullName))
                {
                    mapping.ClientValueExtractorFunctionName = Config.Extractors[control.GetType().FullName];
                }
                else
                {
                    mapping.ClientValueExtractorFunctionName = Config.Extractors["Default"];
                }

                mapping.ClientID = control.ClientID;
            }
            else
            {
                if (String.IsNullOrEmpty(mapping.ClientID))
                {
                    throw new InvalidProgramException("ParameterMapping requires ControlID if ClientID is not specified");
                }

                if (String.IsNullOrEmpty(mapping.ClientValueExtractorFunctionName))
                {
                    mapping.ClientValueExtractorFunctionName = Config.Extractors["Default"];                    
                }
            }
        }

        /// <summary>
        /// Attaches callback trigger events to designated controls
        /// </summary>
        private void AttachCallbackTriggerClientEvents()
        {
            foreach (CallbackTrigger trigger in _Triggers)
            {
                WebControl control = (WebControl)FindControl(trigger.ControlID);
                if (control == null)
                    throw new InvalidProgramException("Error in CallbackTrigger, a control with the id of " + trigger.ControlID + " could not be found");

                if (String.IsNullOrEmpty(trigger.ClientEvent))
                    throw new InvalidProgramException("CallbackTrigger requires ClientEvent to be set");

                control.Attributes.Add(trigger.ClientEvent, "AjaxSnippets.SnippetManager.callback('" + this.ID + "'); return false;");
            }
        }

        public override Control FindControl(string id)
        {
            Control container = this.NamingContainer;
            Control control = null;
            while (container != null)
            {
                control = container.FindControl(id);
                if (control != null) break;
                container = container.NamingContainer;
            }
            return control;
        }

        private void EnsureRequiredProperties()
        {
            if (String.IsNullOrEmpty(this.CallbackSuccessMethodName))
                throw new InvalidProgramException("CallbackSuccessMethodName is required");

            if (String.IsNullOrEmpty(this.CallbackFailureMethodName))
                throw new InvalidProgramException("CallbackFailureMethodName is required");

            if (String.IsNullOrEmpty(this.BeforeCallbackMethodName))
                throw new InvalidProgramException("BeforeCallbackMethodName is required");

            if (String.IsNullOrEmpty(this.WebServiceVirtualPath))
                throw new InvalidProgramException("WebServiceVirtualPath is required");

            if (String.IsNullOrEmpty(this.WebServiceMethodName))
                throw new InvalidProgramException("WebServiceMethodName is required");
        }

        public bool IsOnSnippetPage
        {
            get
            {
                return (this.Page is AjaxSnippets.Snippet.SnippetPage);
            }
        }
    }

}
